Python3によるオブジェクト指向プログラミング - ポリモーフィズム
著者:Leonardo Giordani - 21/08/2014 Updated on Dec 21, 2018
この記事はIPython Notebookとして公開 されています。 おはよう、ポリモーフィズムです
ポリモーフィズム(polymorphism) とは、OOP用語で、オブジェクトが処理するデータの種類に応じてコードを適応させる能力のことを指します。
ポリモーフィズムには、OOP言語における2つの主要な用途があります。1つ目は、オブジェクトが入力パラメータの種類に応じて、そのメソッドの異なる実装を提供することです。2つ目は、あるデータ型のために書かれたコードが、派生型のデータにも使えることです。つまり、ある型のクラス階層を理解しているメソッドです。
Pythonではポリモーフィズムは重要な概念の一つであり、ビルトインの機能であると言えます。順を追って説明していきましょう。
まず、Pythonでは変数の型は明示的に宣言されないことを知っています。これは、Pythonの変数が型付けされていないことを意味しているわけではないことに注意してください。それどころか、Pythonではすべてのものに型があり、その型が暗黙的に割り当てられているのです。前の記事の最後の段落を覚えていれば、Pythonの変数は(C言語のような命名法を使って)単なるポインタであり、言い換えれば、変数がメモリ上のどこに格納されているかを言語に伝えるだけだと述べました。そのアドレスに格納されているものは、その変数のビジネスではありません。
code: python
>> a = 5
>> a
5
>> type(a)
<class 'int'>
>> hex(id(a))
'0x83fe540'
>> a = 'five'
>> a
'five'
>> type(a)
<class 'str'>
>> hex(id(a))
'0xb70d6560'
この小さな例は、Pythonの型付けシステムについて多くのことを示しています。変数 a は静的に宣言されておらず、結局、1種類のデータ、つまりメモリアドレスしか格納できません。5という数字を代入すると、Pythonは a に5という数字のアドレス(私の場合は0x83fe540ですが、あなたの結果は違うでしょう)を格納します。type()組み込み関数は賢いので、a の型(これは常に参照)を聞いているのではなく、中身の型を聞いていることを理解しています。a に別の値、文字列'five'を格納すると、Pythonは臆面もなく、変数の以前の内容を新しいアドレスに置き換えます。
このように、参照システムのおかげで、Pythonの型システムは強力かつ動的なものとなっています。この2つの概念の正確な定義は普遍的なものではありませんので、興味のある方は広い範囲の問題に飛び込む準備をしてください。しかし、Pythonでは、この2つの言葉の意味は次のようになります。
型システムは強力である。なぜなら、すべてのものはきちんと定義された型を持っていて、組み込み関数のtype()でそれを確認できるからである。
変数の型は明示的に宣言されるのではなく、内容に応じて変化するので、型システムは動的である
ここまでは、全体の表面をなぞっただけです。
このテーマをもう少し掘り下げるために、Pythonで最も単純な関数を定義してみましょう(空の関数を除いて)。
code: python
def echo(a):
return a
この関数は期待通りに動作し、与えられたパラメータをエコーします。
code: python
>> echo(5)
5
>> echo('five')
'five'
とてもわかりやすいでしょう?しかし、CやC++のような静的にコンパイルされた言語から来た人は、少なくとも戸惑うはずです。それはつまり、どのようなタイプのデータを含んでいるのかということです。さらに、型の指定がない場合、Pythonはどうやってそれが何を返しているかを知ることができるのでしょうか?
繰り返しになりますが、参照の話を思い出していただければ、すべてが明らかになります。この関数は参照を受け取り、参照を返します。言い換えれば、私たちはある種の普遍的な関数を定義しただけで、それは入力に関係なく同じことをするのです。
これはまさにポリモーフィズムが解決しようとしている問題です。私たちは、オブジェクトの種類に関係なく動作を記述したいのですが、これは人間同士の会話でも同じです。物体を押して動かす方法を説明するとき、箱を使って説明することもあるでしょうが、ペンや本、瓶を動かす必要があっても、相手がその動作を再現できることを期待します。
入力の種類に関わらず、同じ操作を行うコードを得るためには、主に2つの戦略があります。
1つ目は、すべてのケースをカバーするというもので、これは手続き型言語の典型的なアプローチです。整数、浮動小数点、複素数の2つの数値を合計する必要がある場合、3つのsum()関数を書く必要があります。1つは整数型、2つ目は浮動小数点型、3つ目は複素数型にバインドされており、入力型に応じて正しい実装を選択する言語機能を備えています。このロジックは、コンパイラ(言語が静的に型付けされている場合)またはランタイム環境(言語が動的に型付けされている場合)によって実装することができ、C++で採用されているアプローチです。この方法の欠点は,プログラマがすべての可能な状況を予測しなければならないことです:整数と浮動小数点数の合計が必要な場合は?整数と浮動小数点数の和が必要な場合は?(C++では、演算子のオーバーロードにより、このようなケースにも対応できるようになっていますが、この言語の基本的なポリモーフィズム戦略は、ここで公開されているものであることに注意してください)。
2つ目の戦略は、Pythonで実装されているもので、単純に入力オブジェクトに問題を解決してもらうというものです。言い換えれば、問題を逆にして、データ自身に操作をしてもらうのです。可能なすべての型を、可能なすべての組み合わせで合計する関数をたくさん書く代わりに、入力データがそれを行う方法を知っていると信じて、合計することを要求する関数を1つだけ書きます。複雑に聞こえますか?そんなことはありません。
Pythonの+演算子の実装を見てみましょう。c = a + b と書くと、Pythonは実際にc = a.__add__(b) を実行します。ご覧の通り、和の演算は最初の入力変数に委ねられています。つまり、次のように書くと:
code: python
def sum(a, b):
return a + b
この場合、2つの入力変数の型を指定する必要はありません。これはポリモーフィズムの概念を非常に美しくシンプルに実装したものです。Pythonの関数がポリモーフィズムを持つのは、単純にすべてを受け入れ、入力データを信頼して何らかのアクションを実行できるからです。
先に進む前に,もう一つの簡単な例を考えてみましょう。組み込み関数 len() は,入力オブジェクトの長さを返します。例えば:
code: python
>> len(l)
3
>> s = "Just a sentence"
>> len(s)
15
ご覧のように,これは完全なポリモーフィズムを持っています.リストでも文字列でも,単にその長さを計算するだけです.どのような型でも動作するのか,確認してみましょう。
code: python
>> d = {'a': 1, 'b': 2}
>> len(d)
2
>> i = 5
>> len(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
おっと、 len() 関数は、辞書を扱うには十分賢いが、整数は扱えないようだ。 結局のところ、整数に長さは定義されていないんだ。
実際、これこそが Python のポリモーフィズムのポイントで、整数型は長さの演算を定義していないのです。あなたは len() 関数のせいにしていますが、int 型にも責任があります。len()関数は、このコードからわかるように、入力オブジェクトの__len__()メソッドを呼び出すだけです。
code: python
>> l.__len__()
3
>> s.__len__()
15
>> d.__len__()
2
>> i.__len__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__len__'
とても簡単です。int オブジェクトは __len__() メソッドを定義していません。
というわけで、ここまでの知見をまとめると、Pythonのポリモーフィズムは委譲に基づいていると言えるでしょう。以下の章では、EAFP Pythonの原則について説明しますが、委譲の原則がこの言語にはどこにでもあることがわかると思います。
タイプ・ハード
ポリモーフィズムがプログラミング言語に持ち込もうとしているもう一つの現実的な概念は、クラス階層を歩く能力、つまり特化した型の上でコードを実行する能力です。私たちが日常的に行っていることを、複雑な文章で表現していますが、例を挙げれば一目瞭然です。
あなたはドアの開け方を知っています。それは幼い頃に学んだことです。OOPの観点からすると、あなたは、ヒンジで回転する木製の長方形と相互作用することができるオブジェクト(申し訳ありませんが、謙遜ではありません)です。ドアを開けることができるということは、窓も開けることができるということでもあります。また、車のドアも開けることができますが、これも特殊なタイプです(通常のドアと窓の中間のようなものです)。これは、最も一般的な型(基本的なドア)の操作方法を知っていれば、特殊な型(窓、車のドア)であっても、祖先の型と同じように動作すれば(例えば、ヒンジで回転すれば)、操作できることを示しています。
これは、OOP言語に直接反映されます。ポリモーフィズムは、与えられた型のために書かれたコードが、派生型の上でも実行できることを要求します。例えば、"数字 "を格納できるリスト(Pythonではなく、一般的なリストオブジェクト)は、整数を受け入れることができます。このリストには、数字同士が比較できることを必要とする順序付け操作を指定することができます。つまり、整数がお互いを比較する方法を指定するとすぐに、リストに挿入して順序付けすることができます。
静的にコンパイルされた言語は、ポリモーフィズムの概念のこの部分を実装するために、特定の言語機能を提供しなければならない。例えば、C++では、親クラスと子クラスの間にポインタの互換性の概念を導入する必要がある。
Pythonでは、サブタイプポリモーフィズムを実装するための特別な言語機能を提供する必要はありません。すでに発見したように、Pythonの関数は型をチェックせずに任意の変数を受け入れ、正しいメソッドを提供するために変数自体に依存しています。しかし、サブタイプは親型のメソッドを再定義するか、暗黙の委任によって提供しなければならないことはすでに知っているので、Pythonは最初からサブタイプポリモーフィズムを実装していることがわかります。
これは、この言語を扱う際に理解すべき最も重要なことの1つだと思います。Pythonは扱っている変数の実際の型にはあまり興味がありません。Pythonが興味を持っているのは、その変数がどのように動作するかということであり、つまり、その変数が正しいメソッドを提供することを望んでいるのです。そのため、もしあなたが静的型付けされた言語から来たのであれば、「型」ではなく「振る舞い」について考えるよう、特別な努力をする必要があります。これをダックタイピング(Duck Typing) と呼んでいます。
では、例を挙げてみましょう。Roomクラスを定義してみましょう。
code: python
class Room:
def __init__(self, door):
self.door = door
def open(self):
self.door.open()
def close(self):
self.door.close()
def is_open(self):
return self.door.is_open()
ご覧のように非常にシンプルなクラスで、ポリモーフィズムを例示するには十分です。Roomクラスはdoorという変数を受け取りますが、この変数の型は指定されていません。ドアの実際の型が宣言されていないので、言語に組み込まれた「受け入れテスト」がありません。実際、入力される変数は、Roomクラスで使用される次のメソッドをエクスポートしなければなりません:open()、close()、 is_open().。ですから、次のようなクラスを作ることができます。
code: python
class Door:
def __init__(self):
self.status = "closed"
def open(self):
self.status = "open"
def close(self):
self.status = "closed"
def is_open(self):
return self.status == "open"
class BooleanDoor:
def __init__(self):
self.status = False
def open(self):
self.status = True
def close(self):
self.status = False
def is_open(self):
return self.status
最初のクラスは文字列を使用し、2番目のクラスはブール値を使用します。最初のクラスは文字列を使用し、2番目のクラスはブール値を使用します。2つの異なるタイプであるにもかかわらず、どちらも同じように動作するので、どちらもRoomオブジェクトを作成するのに使用できます。
code: python
>> door = Door()
>> bool_door = BooleanDoor()
>> room = Room(door)
>> bool_room = Room(bool_door)
>> room.open()
>> room.is_open()
True
>> room.close()
>> room.is_open()
False
>> bool_room.open()
>> bool_room.is_open()
True
>> bool_room.close()
>> bool_room.is_open()
False
ファイルライクなオブジェクト
ファイルライクオブジェクト(File-Like Object) は、Pythonにおけるポリモーフィズムの具体的で非常に便利な例です。ファイルライクオブジェクトとは、ファイルのように動作するクラス(またはクラスのインスタンス)のことで、ファイルオブジェクトが公開するメソッドを提供します。
例えば、XMLツリーを解析するクラスをコーディングし、そのXMLコードがファイルに含まれていることを期待しているとします。このクラスは、__init__() メソッドでファイルを受け取り、そこからコンテンツを読み込みます。
code: python
class XMLReader:
def __init__(xmlfile):
xmlfile.open()
self.content = xmlfile.read()
xmlfile.close()
このクラスは、ネットワーク・ストリームからXMLコンテンツを受信するようにアプリケーションを変更するまではうまく機能します。クラスを変更せずに使用するには、一時ファイルにストリームを書き込み、後者をロードする必要がありますが、これは少しやりすぎのようです。そこで、クラスを文字列を受け取るように変更することを計画しますが、この方法では、クラスを使ってファイルを読むコードをすべて変更しなければなりません。
ポリモーフィズムはより良い方法を提供します。実際のファイルではなくても、ファイルのように振る舞うオブジェクトの中に、入ってくるストリームを保存してはどうでしょうか?ioモジュールをチェックすれば、そのようなオブジェクトがすでに発明され、Pythonの標準ライブラリで提供されていることがわかります。
他の非常に便利なファイルライクなクラスは、gzip、bz2、zipfile モジュールに含まれているものです (最も使われているものの一部です)。
許しがたいもの
EAFPとは Easier to Ask for Forgiveness than Permission の頭文字をとった言葉です。意味としては、許可よりも許しを求める方が簡単だということになります。Python では「問題が起こってから対処する」という考え方を表すもので、このコーディングスタイルはPythonコミュニティで非常に支持されています。なぜなら、ダックタイピングのコンセプトに完全に依存しているため、言語哲学によく適合しているからです。
EAFPのコンセプトはとても簡単で、あるオブジェクトが与えられた属性やメソッドを持っているかどうかを、実際にアクセスしたり使用したりする前にチェックする代わりに、必要なものを提供してくれるオブジェクトを信頼し、エラーケースを管理するというものです。これを理解するには、いくつかのコードを見てみるとよいでしょう。
EAFPによると、以下のように書く代わりに:
code: pytohn
if hasattr(someobj, 'open'):
else:
こう書くべきです。
code: python
try:
someobj.open()
except AttributeError:
ご覧のように、2つ目のスニペットはメソッドを直接使用し、AttributeErrorの例外を処理しています(ちなみに、例外の管理はPythonの黒魔術のトップトピックの1つですが、これについては今後の記事でご紹介します)。とても簡単なプレビューです。Erlangから何か学べるかもしれません Error handling in Erlang - a primer をご覧ください)。 なぜこのようなコーディングスタイルがPythonコミュニティでこれほど推されているのでしょうか?オブジェクトがopen属性を持っているかどうかを知りたいのではなく、オブジェクトがあなたの要求を満たすことができるかどうか、つまりopen()メソッドの呼び出しを行うことができるかどうかを知りたいのです。
映画のトリビア
セクションのタイトルは以下の映画に由来しています。
Good Morning, Vietnam (1987): Good Morning, Polymorphism / おはよう、ポリモーフィズムです
この映画タイトルの邦題は「グッドモーニング, ベトナム」
Die Hard (1988): Type Hard / タイプ・ハード
映画タイトルの意味通りでは「なかなか死なない」ことなのですが、うまい訳を見つけられず安易に選びました。
この映画タイトルの邦題は「ダイ・ハード」
Spies Like Us (1985): File Like Us / ファイルライクなオブジェクト
これもファイルライクなオブジェクトに言及がなければ難しいかった
この映画タイトルの邦題は「スパイ・ライク・アス」
Unforgiven (1992): Unforgiveness / 許しがたいもの
この映画タイトルの邦題は「許されざる者」
参考資料
Python3によるオブジェクト指向プログラミングシリーズ
日本語訳:Python3によるオブジェクト指向プログラミング - ポリモーフィズム